package com.syf.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

/**
 * 1.授权服务器的配置:
 *
 * @AutoWried按by type自动注入，而@Resource默认按byName自动注入。
 * @Autowired默认是根据类型进行注入的，因此如果有多个类型一样的Bean候选者，则需要限定其中一个候选者，否则将抛出异常
 * @Qualifier限定描述符，除了能根据名字进行注入，更能进行更细粒度的控制如何选择候选者
 *
 */
@Configuration  //使其成为一个配置类
@EnableAuthorizationServer // 开启授权服务器的功能(使其作为认证中心)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder; //密码加密

    @Autowired
    private AuthenticationManager authenticationManager;  //授权服务器

    @Qualifier("userServiceDetailsServiceImpl") //指定接口的 实现类【token验证方法】
    @Autowired
    private UserDetailsService userDetailsService;



    /**
     * [1]配置客户端（应用）信息---添加第三方的客户端:【Postman工具】
     *      配置哪些客户端可以获得授权、授权范围、授权方式、令牌有效期及刷新令牌有效期等信息
     *
     *      1.获取token的其中两种方式(grant_type属性):
     *          password/refresh_token 方式获取token
     *
     *          Authorization:请求身份的认证【Basic Auth基础认证】
     *                   username:coin-api
     *                   password:coin-secret
     *          password:
     *              http://localhost:9999/oauth/token?grant_type=password&username=syf&password=123456&login_type=member_type
     *          refresh_token:
     *              http://localhost:9999/oauth/token?grant_type=refresh_token&login_type=member_type&refresh_token=siac2r2onro2o2bfo2b...
     *
     *      2.获取token的第三种方式:(应用自己去获取一个 临时的 token)
     *          在第一种 Case 里面，我们可以从本次请求的上下文里面获取用户的 token ，进行一个 Token 的传递。
     *          在第二种 Case 里面，我们没有一个用户请求的上下文，因此我们需要应用自己去获取一个 临时的 token。
     *
     *          这 2 种请求的源码实现，OAuth2.0 已经帮我们写好了: 从请求的上下文中获取token,若为null,则去授权服务器获取token
     *          获取的方式非常的简单：使用 client_credentials 客户端模式授权方式来进行的
     *
     *          测试:
     *              Authorization:【请求身份的认证方式: Basic Auth基础认证】
     *                  username:inside-app
     *                  password:inside-secret
     *              client_credentials:
     *                  http://localhost:9999/oauth/token?grant_type=client_credentials
     *
     *
     *      3.SpringSecurityOAuth2配置认证服务器策略：【token包含: grant_type授权模式 和 refresh_token更新令牌】
     *          [1]刷新令牌(refresh_token)模式:
     *              1.如果客服端的令牌过期，可以使用刷新令牌更新令牌。这就就可以避免再次通过用户名和密码登录，重新获取
     *                令牌这些麻烦的操作了(刷新令牌只能是授权码和密码模式下有效)
     *              2.参数：
     *                  grant_type（必须）：表示使用的授权模式，此处的值固定为 refresh_token
     *                  refresh_token（必须）：表示早前收到的更新令牌
     *              3.token值包含:
     *                  access_token(token值)、token_type(token类型:bearer)、refresh_token(更新令牌:它里面包含那些信息 和token值的没啥区别)
     *              4.流程:
     *                  先访问获取 access_token 令牌，拿获取到的 access_token 去刷新令牌，获取到最新的 access_token 和 refresh_token。
     *                  如果拿之前的 access_token去访问接口，这个时候是不能访问的，已经被 refresh_token 刷新了，不能继续使用。
     *              5.为什么需要刷新令牌:
     *                  解决【access token设置时间比较长，容易泄露造成安全问题，设置时间比较短，又需要频繁让用户授权】的矛盾
     *              6.刷新令牌的生命周期:
     *                  1)授权服务 颁发 刷新令牌:
     *                      第三方软件得到一个访问令牌的同时，也会得到一个刷新令牌，refresh_token是与第三方软件、用户关联在一起的
     *                  2)第三方服务使用 刷新令牌:
     *                      什么时候来使用刷新令牌呢？
     *                          (1)定时检测方式:
     *                              在第三方软件收到访问令牌的同时，也会收到访问令牌的过期时间expires_in。一个设计良好的第三方应用，
     *                              应该将expires_in值保存下来并定时检测；如果发现expires_in “即将过期”，则需要利用refresh_token去重新请求授权服务，以便获取新的、有效的访问令牌。
     *                          (2)现场发现方式:
     *                              比如第三方软件访问 受保护资源的时候，突然收到一个访问令牌失效的响应，此时第三方软件立即使用refresh_token来请求一个访问令牌，以便继续代表用户使用他的数据。
     *
     *                          综合来看的话，定时检测的方式，需要额外开发一个定时任务；而“现场”发现，就没有这种额外的工作量。
     *                          具体采用哪一种方式，你可以结合自己的实际情况。不过呢，建议采用定时检测这种方式，因为它可以带来“提前量”，以便有更好的主动性，而现场发现就有点被动了。
     *                  3)授权服务 校验刷新令牌:
     *                      第三方软件发送请求,授权服务器会先比较grant_type和 refresh_token的值，确认是请求刷新令牌的操作
     *                      1. 接收刷新令牌请求，验证基本信息
     *                      2. 重新生成访问令牌
     *
     *          [2]密码模式(password)
     *          [3]客户端模式(client_credentials)
     *          [4]授权码模式(authorization_code)(注意是get请求方式)
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //1.密码模式和刷新令牌(refresh_token)模式:
                .withClient("coin-api")// 第三方客户端id(username - 自己随意起)
                .secret(passwordEncoder.encode("coin-secret"))// 第三方客户端密码(password - 随意起),要加密，不然要求一直要登录，获取不到令牌，而且一定不要被泄露
                .scopes("all") //配置授权范围 read、write、all，哪部分资源可以访问
                //授权模式(可同时支持多种类型): 可用password/refresh_token方式获取token(下边分别有它们获取的token有效期)
                .authorizedGrantTypes("password","refresh_token",    "authorization_code","implicit")  //授权码认证模式和 简化认证模式(安全度低)
                .accessTokenValiditySeconds(7 * 24 *3600) // token令牌有效期 单位s
                //refreshtoken刷新令牌有效期 单位s 一般来说刷新令牌比令牌有效期长 便于使用刷新令牌换取令牌
                .refreshTokenValiditySeconds(30 * 24 * 3600)
                // 3、指定重定向地址: 授权码认证模式和简化认证模式中必须
                .redirectUris("http://www.baidu.com")

                //2.客户端授权方式: 应用自己去获取一个 临时的 token
                .and()
                .withClient("inside-app")
                .secret(passwordEncoder.encode("inside-secret"))
                .authorizedGrantTypes("client_credentials") //客户端授权模式
                .scopes("all")
                //内部Token,永不过期，写死 -》 powman请求一个token,将其设为常量 【OAuth2FeignConfig】
                .accessTokenValiditySeconds(Integer.MAX_VALUE);

        super.configure(clients);
    }

    /**
     * [2]设置授权管理器 和 UserDetailsService(配置授权服务器端点的属性和增强的功能):
     *  一、令牌管理策略:
     *      0.默认情况下，令牌是通过 randomUUID 产生32位随机数的来进行填充的，而产生的令牌默认是存储在内存中。
     *      1.内存采用TokenStore 接口的默认实现类 InMemoryTokenStore , 开发时方便调试，适用单机版。
     *      2.RedisTokenStore 将令牌存储到 Redis 非关系型数据库中，适用于并发高的服务。
     *      3.JdbcTokenStore 基于 JDBC 将令牌存储到 关系型数据库中，可以在不同的服务器之间共享令牌。
     *      4.JwtTokenStore （JSON Web Token）将用户信息直接编码到令牌中，这样后端可以不用存储它，前端拿到令牌可以直接解析出用户信息【优选】
     *
     *  1)authenticationManager: 【密码模式： 请求token + 验证】
     *      授权中心在对第三方，携带access token的请求做出回应时，需要"验证" 资源所有者的证书
     *
     *  2)userDetailsService:【刷新令牌认证模式：引入实现的userDeatils接口的实现类】
     *      将myUserdDetailsService交给服务器端点
     *
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)  //授权服务器(将authenticationManager交给服务器端点)
                .userDetailsService(userDetailsService) //user的service【获取用户密码账号】

                .tokenStore(jwtTokenStore())// tokenStore 来存储我们的token     【用jwt 来存储token】
                .tokenEnhancer(jwtAccessTokenConverter());  //jwt存储时需要指定 【token的生成方式】

        super.configure(endpoints);
    }

    //jwt 存储token ("access_token"属性存储)
    private TokenStore jwtTokenStore() {
        JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());
        return jwtTokenStore;
    }

    //token的生成方式
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();

        // 加载我们的私钥
        ClassPathResource classPathResource = new ClassPathResource("coinexchange.jks");
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, "coinexchange".toCharArray());
        tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("coinexchange", "coinexchange".toCharArray()));
        return tokenConverter;
    }

}
